//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Text;
using EffiProz.Core.Rights;

namespace EffiProz.Core
{
    /**
     * A simple structure class for holding the products of
     * statement compilation for later execution.
     *
     * @author  boucherb@users
     * @version 1.7.2
     * @since 1.7.2
     */

    // fredt@users 20040404 - patch 1.7.2 - fixed type resolution for parameters
    // boucherb@users 200404xx - patch 1.7.2 - changed parameter naming scheme for SQLCI client usability/support
    // fredt@users 20050609 - 1.8.0 - fixed EXPLAIN PLAN by implementing describe(Session)
    public class CompiledStatement
    {

        public const string PCOL_PREFIX = "@p";
        public const string RETURN_COLUMN_NAME = "@p0";
        public const int UNKNOWN = 0;

        // enumeration of allowable CompiledStatement types
        public const int INSERT_VALUES = 1;
        public const int INSERT_SELECT = 2;
        public const int UPDATE = 3;
        public const int DELETE = 4;
        public const int SELECT = 5;
        public const int SELECT_INTO = 6;
        public const int CALL = 7;

        // enumeration of catagories
        public const int DML = 7;
        public const int DQL = 8;
        public const int DDL = 9;

        /** id _in CompiledStatementManager */
        public int id;

        /** false when cleared */
        public bool isValid = true;

        /** target table for INSERT_XXX, UPDATE and DELETE */
        public Table targetTable;

        /** table filter for UPDATE and DELETE */
        public TableFilter targetFilter;

        /** condition expression for UPDATE and DELETE */
        public Expression condition;

        /** column map for INSERT_XXX, UPDATE */
        public int[] columnMap;

        /** Column value Expressions for INSERT_VALUES and UPDATE. */
        public Expression[] columnValues;

        /**
         * Flags indicating which columns' values will/will not be
         * explicitly set.
         */
        public bool[] checkColumns;

        /** Expression to be evaluated when this is a CALL statement. */
        public Expression expression;

        /**
         * Select to be evaluated when this is an INSERT_SELECT or
         * SELECT statement
         */
        public Select select;

        /**
         * Parse-order array of Expression objects, all of iType == PARAM ,
         * involved _in some way _in any INSERT_XXX, UPDATE, DELETE, SELECT or
         * CALL CompiledStatement
         */
        public Expression[] parameters;

        /**
         * int[] contains type of each parameter
         */
        int[] paramTypes;

        /**
         * Subqueries inverse parse depth order
         */
        public SubQuery[] subqueries;

        /**
         * The type of this CompiledStatement. <p>
         *
         * One of: <p>
         *
         * <ol>
         *  <li>UNKNOWN
         *  <li>INSERT_VALUES
         *  <li>INSERT_SELECT
         *  <li>UPDATE
         *  <li>DELETE
         *  <li>SELECT
         *  <li>CALL
         *  <li>DDL
         * </ol>
         */
        public int type;

        /**
         * The SQL string that produced this compiled statement
         */
        public string sql;

        /**
         * The default schema name used to resolve names _in the sql
         */
        public QNameManager.QName schemaHsqlName;

        /**
         * Creates a new instance of CompiledStatement for DDL
         *
         */
        public CompiledStatement(QNameManager.QName schema)
        {

            parameters = new Expression[0];
            paramTypes = new int[0];
            subqueries = new SubQuery[0];
            type = DDL;
            schemaHsqlName = schema;
        }

        /**
         * Initializes this as a DELETE statement
         *
         * @param targetFilter
         * @param deleteCondition
         * @param parameters
         */
        public CompiledStatement(Session session, Database database, QNameManager.QName schema,
                          TableFilter targetFilter, Expression deleteCondition,
                          SubQuery[] subqueries,
                          Expression[] _params)
        {

            schemaHsqlName = schema;
            this.targetFilter = targetFilter;
            targetTable = targetFilter.filterTable;

            if (deleteCondition != null)
            {
                condition = new Expression(deleteCondition);

                condition.resolveTables(targetFilter);
                condition.resolveTypes(session);
                targetFilter.setConditions(session, condition);
            }

            setParameters(_params);
            setSubqueries(subqueries);

            type = DELETE;
        }

        /**
         * Instantiate this as an UPDATE statement.
         *
         * @param targetTable
         * @param columnMap
         * @param columnValues
         * @param updateCondition
         * @param _params
         */
        public CompiledStatement(Session session, Database database, QNameManager.QName schema,
                           TableFilter targetFilter, int[] columnMap,
                           Expression[] columnValues, Expression updateCondition,
                           SubQuery[] subqueries,
                           Expression[] _params)
        {

            schemaHsqlName = schema;
            this.targetFilter = targetFilter;
            targetTable = targetFilter.filterTable;
            this.columnMap = columnMap;
            this.columnValues = columnValues;

            for (int i = 0; i < columnValues.Length; i++)
            {
                Expression cve = columnValues[i];

                if (cve.isParam())
                {
                    cve.setTableColumnAttributes(targetTable, columnMap[i]);
                }
                else
                {
                    cve.resolveTables(targetFilter);
                    cve.resolveTypes(session);
                }
            }

            if (updateCondition != null)
            {
                condition = new Expression(updateCondition);

                condition.resolveTables(targetFilter);
                condition.resolveTypes(session);
                targetFilter.setConditions(session, condition);
            }

            setParameters(_params);
            setSubqueries(subqueries);

            type = UPDATE;
        }

        /**
         * Instantiate this as an INSERT_VALUES statement.
         *
         * @param targetTable
         * @param columnMap
         * @param columnValues
         * @param checkColumns
         * @param _params
         */
        public CompiledStatement(QNameManager.QName schema, Table targetTable, int[] columnMap,
                          Expression[] columnValues, bool[] checkColumns,
                          SubQuery[] subqueries,
                          Expression[] _params)
        {

            schemaHsqlName = schema;
            this.targetTable = targetTable;
            this.columnMap = columnMap;
            this.checkColumns = checkColumns;
            this.columnValues = columnValues;

            for (int i = 0; i < columnValues.Length; i++)
            {
                Expression cve = columnValues[i];

                // If its not a param, it's already been resolved in
                // Parser.getColumnValueExpressions
                if (cve.isParam())
                {
                    cve.setTableColumnAttributes(targetTable, columnMap[i]);
                }
            }

            setParameters(_params);
            setSubqueries(subqueries);

            type = INSERT_VALUES;
        }

        /**
         * Instantiate this as an INSERT_SELECT statement.
         *
         * @param targetTable
         * @param columnMap
         * @param checkColumns
         * @param select
         * @param _params
         */
        public CompiledStatement(Session session, Database database, QNameManager.QName schema,
                         Table targetTable, int[] columnMap,
                         bool[] checkColumns, Select select,
                         SubQuery[] subqueries,
                         Expression[] _params)
        {

            schemaHsqlName = schema;
            this.targetTable = targetTable;
            this.columnMap = columnMap;
            this.checkColumns = checkColumns;
            this.select = select;

            // resolve any parameters _in SELECT
            resolveInsertParameterTypes();

            // set select result metadata etc.
            select.prepareResult(session);
            setParameters(_params);
            setSubqueries(subqueries);

            type = INSERT_SELECT;
        }

        /**
         * Instantiate this as a SELECT statement.
         *
         * @param select
         * @param _params
         */
        public CompiledStatement(Session session, Database database, QNameManager.QName schema,
                         Select select, SubQuery[] subqueries,
                         Expression[] _params)
        {

            schemaHsqlName = schema;
            this.select = select;

            // resolve any parameters _in SELECT as VARCHAR
            for (int i = 0; i < select.iResultLen; i++)
            {
                Expression colexpr = select.exprColumns[i];

                if (colexpr.getDataType() == Types.NULL)
                {
                    colexpr.setDataType(Types.VARCHAR);
                }
            }

            // set select result metadata etc.
            select.prepareResult(session);
            setParameters(_params);
            setSubqueries(subqueries);

            type = SELECT;
        }

        /**
         * Instantiate this as a CALL statement.
         *
         * @param expression
         * @param _params
         */
        public CompiledStatement(Session session, Database database, QNameManager.QName schema,
                          Expression expression, SubQuery[] subqueries,
                          Expression[] _params)
        {

            schemaHsqlName = schema;
            this.expression = expression;

            expression.resolveTypes(session);

            expression.paramMode = Expression.PARAM_OUT;

            setParameters(_params);
            setSubqueries(subqueries);

            type = CALL;
        }

        /**
         * For parameters _in INSERT_VALUES and INSERT_SELECT lists
         */
        private void resolveInsertParameterTypes()
        {

            for (int i = 0; i < select.iResultLen; i++)
            {
                Expression colexpr = select.exprColumns[i];

                if (colexpr.getDataType() == Types.NULL)
                {
                    Column col = targetTable.getColumn(columnMap[i]);

                    colexpr.setDataType(col.getType());
                }
            }
        }

        private void setParameters(Expression[] _params)
        {

            this.parameters = _params;

            int[] types = new int[parameters.Length];

            for (int i = 0; i < parameters.Length; i++)
            {
                types[i] = parameters[i].getDataType();
            }

            this.paramTypes = types;
        }

        private void setSubqueries(SubQuery[] subqueries)
        {
            this.subqueries = subqueries;
        }

        public void materializeSubQueries(Session session)
        {

            for (int i = 0; i < subqueries.Length; i++)
            {
                SubQuery sq = subqueries[i];

                // VIEW working table contents are filled only once per query and reused
                if (sq.isMaterialised)
                {
                    continue;
                }

                if (sq.isResolved)
                {
                    sq.populateTable(session);

                    sq.isMaterialised = true;
                }
            }
        }

        public void dematerializeSubQueries(Session session)
        {

            if (subqueries == null)
            {
                return;
            }

            for (int i = 0; i < subqueries.Length; i++)
            {
                subqueries[i].table.clearAllRows(session);

                subqueries[i].isMaterialised = false;
            }
        }

        public void clearVariables()
        {

            isValid = false;
            targetTable = null;
            targetFilter = null;
            condition = null;
            columnMap = null;
            columnValues = null;
            checkColumns = null;
            expression = null;
            select = null;
            parameters = null;
            paramTypes = null;
            subqueries = null;
        }

        public bool canExecute(Session session)
        {

            switch (type)
            {

                case CALL: { }
                    break;
                case SELECT:
                    for (int i = 0; i < select.tFilter.Length; i++)
                    {
                        QNameManager.QName name = select.tFilter[i].filterTable.getName();

                        session.check(name, GrantConstants.SELECT);
                    }
                    break;

                case INSERT_SELECT:
                    break;

                case DELETE:
                    session.check(targetTable.getName(), GrantConstants.DELETE);
                    break;

                case INSERT_VALUES:
                    session.check(targetTable.getName(), GrantConstants.INSERT);
                    break;

                case UPDATE:
                    session.check(targetTable.getName(), GrantConstants.UPDATE);
                    break;

                case DDL:
                    break;
            }

            return true;
        }

        public void checkTableWriteAccess(Session session,
                                   Table table)
        {

            // session level user rights
            session.checkReadWrite();

            // object type
            if (table.isView())
            {
                throw Trace.error(Trace.NOT_A_TABLE, table.getName().name);
            }

            // object _readonly
            table.checkDataReadOnly();
        }

        private static Result updateCountResult =
            new Result(ResultConstants.UPDATECOUNT);

        public Result describeResult()
        {

            switch (type)
            {

                case CALL:
                    {

                        // TODO:
                        //
                        // 1.) standard to register metadata for columns of
                        // the primary result set, if any, generated by call
                        //
                        // 2.) Represent the return value, if any (which is
                        // not, _in truth, a result set), as an OUT parameter
                        //
                        // For now, I've reverted a bunch of code I had _in place
                        // and instead simply reflect things as the are, describing
                        // a single column result set that communicates
                        // the return value.  If the expression generating the
                        // return value has a void return type, a result set
                        // is described whose single column is of type NULL
                        Expression e;
                        Result r;

                        e = expression;
                        r = Result.newSingleColumnResult(
                            CompiledStatement.RETURN_COLUMN_NAME, e.getDataType());
                        r.metaData.classNames[0] = e.getValueClassName();

                        // no more setup for r; all the defaults apply
                        return r;
                    }
                case SELECT:
                    return select.sIntoTable == null ? select.describeResult()
                                                     : updateCountResult;

                case DELETE:
                case INSERT_SELECT:
                case INSERT_VALUES:
                case UPDATE:
                case DDL:

                    // will result in
                    return updateCountResult;

                default:
                    return new Result(
                        Trace.runtimeError(
                            Trace.UNSUPPORTED_INTERNAL_OPERATION,
                            "CompiledStatement.describeResult()"), null);
            }
        }

        public Result describeParameters()
        {

            Result _out;
            Expression e;
            int outlen;
            int offset;
            int idx;
       
            outlen = parameters.Length;
            offset = 0;

            // NO:  Not yet
            //        hasReturnValue = (type == CALL && !expression.isProcedureCall());
            //
            //        if (hasReturnValue) {
            //            outlen++;
            //            offset = 1;
            //        }
            _out = Result.newParameterDescriptionResult(outlen);

            // NO: Not yet
            //        if (hasReturnValue) {
            //            e = expression;
            //            _out.sName[0]       = DIProcedureInfo.RETURN_COLUMN_NAME;
            //            _out.sClassName[0]  = e.getValueClassName();
            //            _out.colType[0]     = e.getDataType();
            //            _out.colSize[0]     = e.getColumnSize();
            //            _out.colScale[0]    = e.getColumnScale();
            //            _out.nullability[0] = e.nullability;
            //            _out.isIdentity[0]  = false;
            //            _out.paramMode[0]   = expression.PARAM_OUT;
            //        }
            for (int i = 0; i < parameters.Length; i++)
            {
                e = parameters[i];
                idx = i + offset;

                // always i + 1.  We currently use the convention of @p0 to name the
                // return value OUT parameter
                _out.metaData.colNames[idx] = e.getParamName() == null ? CompiledStatement.PCOL_PREFIX
                    + (i + 1) : e.getParamName();

                // sLabel is meaningless _in this context.
                _out.metaData.classNames[idx] = e.getValueClassName();
                _out.metaData.colTypes[idx] = e.getDataType();
                _out.metaData.colSizes[idx] = e.getColumnSize();
                _out.metaData.colScales[idx] = e.getColumnScale();
                _out.metaData.colNullable[idx] = e.nullability;
                _out.metaData.isIdentity[idx] = e.isIdentity;

                // currently will always be Expression.PARAM_IN
                _out.metaData.paramMode[idx] = e.paramMode;
            }

            return _out;
        }

        /**
         * Retrieves a string representation of this object.
         *
         * @return  the string representation of this object
         */
        public string describe(Session session)
        {

            try
            {
                return describeImpl(session);
            }
            catch (Exception e)
            {
                return e.ToString();
            }
        }

        /**
         * Provides the ToString() implementation.
         *
         * @  if a database access or io error occurs
         * @return the string representation of this object
         */
        private string describeImpl(Session session)
        {

            StringBuilder sb;

            sb = new StringBuilder();

            switch (type)
            {

                case SELECT:
                    {
                        sb.Append(select.describe(session));
                        appendParms(sb).Append('\n');
                        appendSubqueries(sb);

                        return sb.ToString();
                    }
                case INSERT_VALUES:
                    {
                        sb.Append("INSERT VALUES");
                        sb.Append('[').Append('\n');
                        appendColumns(sb).Append('\n');
                        appendTable(sb).Append('\n');
                        appendParms(sb).Append('\n');
                        appendSubqueries(sb).Append(']');

                        return sb.ToString();
                    }
                case INSERT_SELECT:
                    {
                        sb.Append("INSERT SELECT");
                        sb.Append('[').Append('\n');
                        appendColumns(sb).Append('\n');
                        appendTable(sb).Append('\n');
                        sb.Append(select.describe(session)).Append('\n');
                        appendParms(sb).Append('\n');
                        appendSubqueries(sb).Append(']');

                        return sb.ToString();
                    }
                case UPDATE:
                    {
                        sb.Append("UPDATE");
                        sb.Append('[').Append('\n');
                        appendColumns(sb).Append('\n');
                        appendTable(sb).Append('\n');
                        appendCondition(session, sb);
                        sb.Append(targetFilter.describe(session)).Append('\n');
                        appendParms(sb).Append('\n');
                        appendSubqueries(sb).Append(']');

                        return sb.ToString();
                    }
                case DELETE:
                    {
                        sb.Append("DELETE");
                        sb.Append('[').Append('\n');
                        appendTable(sb).Append('\n');
                        appendCondition(session, sb);
                        sb.Append(targetFilter.describe(session)).Append('\n');
                        appendParms(sb).Append('\n');
                        appendSubqueries(sb).Append(']');

                        return sb.ToString();
                    }
                case CALL:
                    {
                        sb.Append("CALL");
                        sb.Append('[');
                        sb.Append(expression.describe(session)).Append('\n');
                        appendParms(sb).Append('\n');
                        appendSubqueries(sb).Append(']');

                        return sb.ToString();
                    }
                default:
                    {
                        return "UNKNOWN";
                    }
            }
        }

        private StringBuilder appendSubqueries(StringBuilder sb)
        {

            sb.Append("SUBQUERIES[");

            for (int i = 0; i < subqueries.Length; i++)
            {
                sb.Append("\n[level=").Append(subqueries[i].level).Append(
                    '\n').Append("hasParams=").Append(
                    subqueries[i].hasParams).Append('\n');

                if (subqueries[i].select != null)
                {
                    sb.Append("org.hsqldb.Select@").Append(String.Format("{0:X2}", subqueries[i].select.GetHashCode()));
                }

                sb.Append("]");
            }

            sb.Append(']');

            return sb;
        }

        private StringBuilder appendTable(StringBuilder sb)
        {

            sb.Append("TABLE[").Append(targetTable.getName().name).Append(']');

            return sb;
        }

        private StringBuilder appendColumns(StringBuilder sb)
        {

            sb.Append("COLUMNS=[");

            for (int i = 0; i < columnMap.Length; i++)
            {
                sb.Append('\n').Append(columnMap[i]).Append(':').Append(
                    ' ').Append(
                    targetTable.getColumn(columnMap[i]).columnName.name).Append(
                    '[').Append(columnValues[i]).Append(']');
            }

            sb.Append(']');

            return sb;
        }

        private StringBuilder appendParms(StringBuilder sb)
        {

            sb.Append("PARAMETERS=[");

            for (int i = 0; i < parameters.Length; i++)
            {
                sb.Append('\n').Append('@').Append(i).Append('[').Append(
                    parameters[i]).Append(']');
            }

            sb.Append(']');

            return sb;
        }

        private StringBuilder appendCondition(Session session, StringBuilder sb)
        {

            return condition == null ? sb.Append("CONDITION[]\n")
                                     : sb.Append("CONDITION[").Append(
                                         condition.describe(session)).Append(
                                         "]\n");
        }
    }
}